﻿using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Diagnostics;

namespace SharpGL
{
   	/// <summary>
	/// This is the basic OpenGL control object, it gives all of the basic OpenGL functionality.
	/// </summary>
    [System.Drawing.ToolboxBitmap(typeof(OpenGLControl), "SharpGL.png")]
    public partial class OpenGLControl : UserControl
    {
        #region Private Fields

        /// <summary>
        /// The render context type.
        /// </summary>
        private RenderContextType renderContextType = RenderContextType.NativeWindow;

        /// <summary>
        /// The bit depth.
        /// </summary>
        private int bitDepth = 32;

        /// <summary>
        /// The OpenGL object for the control.
        /// </summary>
        private OpenGL currentOpenGLContext = new OpenGL();

        /// <summary>
        /// When set to true, the draw time will be displayed in the render.
        /// </summary>
        private bool drawFPS = false;

        /// <summary>
        /// The draw frame rate by timer.
        /// </summary>
        private uint frameRate = 24;

        /// <summary>
        /// The timer used for drawing the control.
        /// </summary>
        private Timer timerDrawing = new Timer();

        /// <summary>
        /// The last frame time in milliseconds.
        /// </summary>
        private double frameTime = 1000.0d;

        /// <summary>
        /// Font used for render the draw time.
        /// </summary>
        private Font drawFPSFont = new Font("Courier New", 12.0f);

        /// <summary>
        /// a stopwatch used for timing rendering.
        /// </summary>
        private Stopwatch stopwatch = new Stopwatch();

        #endregion Private Fields

        #region Private Methods

        /// <summary>
        /// Initialises OpenGL context.
        /// </summary>
        private bool InitializeOpenGL(RenderContextType renderContextType, int bitDepth, int width, int height, IntPtr windowHandle)
        {
            bool result = false;
            object parameter = null;

            //  Native render context providers need a little bit more attention.
            if (renderContextType == RenderContextType.NativeWindow)
            {
                SetStyle(ControlStyles.OptimizedDoubleBuffer, false);
                parameter = (object)windowHandle;
            }

            if (renderContextType != SharpGL.RenderContextType.NativeWindow || parameter != null)
            {
                //  Create the render context.
                result = currentOpenGLContext.Create(renderContextType, width, height, bitDepth, parameter);
                
                if (result != false)
                {
                    OnOpenGLContextInitialized();
                    DoOpenGLContextInitialized();
                }
            }

            return result;
        }

        /// <summary>
        /// Handles the Tick event of the timerDrawing control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
        private void OnDrawingTimer(object sender, EventArgs e)
        {
            //  The timer for drawing simply invalidates the control at a regular interval.
            Invalidate();
        }

        /// <summary>
        /// Free OpenGL context resources.
        /// </summary>
        protected bool ReleaseOpenGL()
        {
            OnOpenGLContextReleasing();
            DoOpenGLContextReleasing();
            
            currentOpenGLContext.Destroy();

            return true;
        }

        #endregion Private Methods

        #region Protected Methods

        /// <summary> 
        /// Clean up any resources being used.
        /// </summary>
        /// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                    components.Dispose();

                if (drawFPSFont != null)
                    drawFPSFont.Dispose();

                if (OpenGL.IsValidContext(currentOpenGLContext))
                    ReleaseOpenGL();
            }

            base.Dispose(disposing);
        }

        /// <summary>
        /// Call this function in derived classes to do the GDI Draw event.
        /// </summary>
        protected void DoGDIDraw(PaintEventArgs e)
        {
            var handler = GDIDraw;
            if (handler != null)
                handler(this, e);
        }

        /// <summary>
        /// Call this function in derived classes to do the OpenGL Draw event.
        /// </summary>
        protected void DoOpenGLDraw(PaintEventArgs e)
        {
            var handler = OpenGLDraw;
            if (handler != null)
                handler(this, e);
        }

        /// <summary>
        /// Calls the OpenGL initialized function.
        /// </summary>
        protected void DoOpenGLContextInitialized()
        {
            var handler = OpenGLContextInitialized;
            if (handler != null)
                handler(this, null);
        }

        /// <summary>
        /// Calls the OpenGL releasing function.
        /// </summary>
        protected void DoOpenGLContextReleasing()
        {
            var handler = OpenGLContextReleasing;
            if (handler != null)
                handler(this, null);
        }

        protected override void OnHandleCreated(EventArgs e)
        {
            base.OnHandleCreated(e);

            if (InitializeOpenGL(renderContextType, bitDepth, Width, Height, Handle) == false)
                throw new Exception("Can't initialize OpenGL context.");

            OnSizeChanged(null);
        }

        protected override void OnHandleDestroyed(EventArgs e)
        {
            if (OpenGL.IsValidContext(currentOpenGLContext))
                ReleaseOpenGL();

            base.OnHandleDestroyed(e);
        }

        protected virtual void OnOpenGLContextInitialized()
        {
        }

        protected virtual void OnOpenGLContextReleasing()
        {
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.Paint"/> event.
        /// </summary>
        /// <param name="e">a <see cref="T:System.Windows.Forms.PaintEventArgs"/> that contains the event data.</param>
        protected override void OnPaint(PaintEventArgs e)
        {
            //  Start the stopwatch so that we can time the rendering.
            stopwatch.Restart();

            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                //	Make sure it's our instance of OpenGL that's active.
                currentOpenGLContext.MakeCurrent();

                //	If there is a draw handler, then call it.
                DoOpenGLDraw(e);

                //  Draw the FPS.
                if (DrawFPS)
                {
                    currentOpenGLContext.DrawText(5, 5, drawFPSFont, 1.0f, 0.0f, 0.0f,
                        string.Format("Draw Time: {0:0.0000} ms ~ {1:0.0} FPS", frameTime, 1000.0 / frameTime));
                    currentOpenGLContext.Flush();
                }

                //	Blit our offscreen bitmap.
                IntPtr handleDeviceContext = IntPtr.Zero;
                try
                {
                    handleDeviceContext = e.Graphics.GetHdc();
                    currentOpenGLContext.Blit(handleDeviceContext);
                }
                finally
                {
                    if (handleDeviceContext != IntPtr.Zero)
                        e.Graphics.ReleaseHdc(handleDeviceContext);
                }

                //  Perform GDI drawing.
                if (currentOpenGLContext.RenderContextProvider.GDIDrawingEnabled != false)
                    DoGDIDraw(e);
            }

            //  Stop the stopwatch.
            stopwatch.Stop();

            //  Store the frame time.
            frameTime = stopwatch.Elapsed.TotalMilliseconds;
        }

        protected override void OnPaintBackground(PaintEventArgs e)
        {
            if (RenderContextType != SharpGL.RenderContextType.NativeWindow)
                base.OnPaintBackground(e);
        }

        /// <summary>
        /// Raises the <see cref="E:System.Windows.Forms.Control.SizeChanged"/> event.
        /// </summary>
        /// <param name="e">An <see cref="T:System.EventArgs"/> that contains the event data.</param>
        protected override void OnSizeChanged(EventArgs e)
        {
            base.OnSizeChanged(e);

            if (OpenGL.IsValidContext(currentOpenGLContext))
            {
                //	Resize the DIB Surface.
                currentOpenGLContext.SetDimensions(Width, Height);

                //  If we have a project handler, call it ...
                if (Width != -1 && Height != -1)
                {
                    //	Set the viewport.
                    currentOpenGLContext.Viewport(0, 0, Width, Height);

                    var handler = Resized;
                    if (handler != null)
                    {
                        handler(this, e);
                    }
                    else
                    {
                        //  Otherwise we do our own projection.
                        currentOpenGLContext.MatrixMode(OpenGL.GL_PROJECTION);
                        currentOpenGLContext.LoadIdentity();

                        // Calculate The Aspect Ratio Of The Window
                        double aspectRatio;
                        if (Height != 0)
                            aspectRatio = (float)Width / (float)Height;
                        else
                            aspectRatio = 1.0f;

                        currentOpenGLContext.Perspective(45.0f, aspectRatio, 0.1f, 100.0f);

                        currentOpenGLContext.MatrixMode(OpenGL.GL_MODELVIEW);
                        currentOpenGLContext.LoadIdentity();
                    }
                }

                Invalidate();
            }
        }

        #endregion Protected Methods

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="OpenGLControl"/> class.
        /// </summary>
        public OpenGLControl()
        {
            InitializeComponent();

            //  Set the user draw styles.
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);

            timerDrawing.Interval = 1000 / (int)FrameRate;
            timerDrawing.Enabled = drawFPS;
        }

        #endregion Constructors

        #region Public Events

        [Description("Called at the point in the render cycle when GDI drawing can occur."), Category("SharpGL")]
        public event PaintEventHandler GDIDraw;

        [Description("Called whenever OpenGL drawing can should occur."), Category("SharpGL")]
        public event PaintEventHandler OpenGLDraw;

        [Description("Called when OpenGL context has been initialized occur."), Category("SharpGL")]
        public event EventHandler OpenGLContextInitialized;

        [Description("Called when OpenGL context has been releasing."), Category("SharpGL")]
        public event EventHandler OpenGLContextReleasing;

        [Description("Called when the control is resized - you can use this to do custom viewport projections."), Category("SharpGL")]
        public event EventHandler Resized;

        #endregion Public Events

        #region Public Properties

        [Description("The render context type."), Category("SharpGL")]
        public RenderContextType RenderContextType
        {
            get { return renderContextType; }
            set
            {
                if (renderContextType != value)
                {
                    renderContextType = value;

                    if (OpenGL.IsValidContext(currentOpenGLContext))
                    {
                        ReleaseOpenGL();
                        if (InitializeOpenGL(value, bitDepth, Width, Height, Handle) == false)
                            throw new Exception("Can't initialize OpenGL context.");

                        Invalidate();
                    }
                }
            }
        }

        [Description("The bit depth."), Category("SharpGL")]
        public int BitDepth
        {
            get { return bitDepth; }
            set
            {
                if (bitDepth != value)
                {
                    bitDepth = value;

                    if (OpenGL.IsValidContext(currentOpenGLContext))
                    {
                        ReleaseOpenGL();
                        if (InitializeOpenGL(renderContextType, value, Width, Height, Handle) == false)
                            throw new Exception("Can't initialize OpenGL context.");

                        Invalidate();
                    }
                }
            }
        }

        /// <summary>
        /// Gets the OpenGL object.
        /// </summary>
        /// <value>The OpenGL.</value>
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public OpenGL CurrentOpenGLContext
        {
            get { return currentOpenGLContext; }
            protected set { currentOpenGLContext = value; }
        }

        [Description("Should the draw time be shown?"), Category("SharpGL")]
        public bool DrawFPS
        {
            get { return drawFPS; }
            set
            {
                if (drawFPS != value)
                {
                    drawFPS = value;

                    Invalidate();
                }
            }
        }

        [Description("The rate at which the control should be re-drawn, in Hertz."), Category("SharpGL")]
        public uint FrameRate
        {
            get { return frameRate; }
            set
            {
                frameRate = value;
                if (frameRate > 0)
                    timerDrawing.Interval = 1000 / (int)frameRate;
                else
                    timerDrawing.Interval = 1000;
            }
        }

        [Description("Enable control to be re-drawn by timer."), Category("SharpGL")]
        public bool EnableTimerDrawing
        {
            get { return timerDrawing.Enabled; }
            set { timerDrawing.Enabled = value; }
        }

        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        public double FrameTime
        {
            get { return frameTime; }
            protected set { frameTime = value; }
        }

        #endregion Public Properties
    }
}
